Rustで組み込みプログラミングの第一歩、LチカとHello Worldを試してみた
最近社内ではRustがちょっと流行ってきていて、社内勉強会を開催したりしています。
一方、社内の研究開発として、組み込みのプロトタイピングをやったりしています(たとえばDevelopersIO Cafeでも使っています)。
Rustは、システムプログラミングに向いているとも言われています。Rustで組み込みするのも一興かと思いますので、実際に試してみました。
やることは、組み込みのHello WorldであるLチカです。
stm32-rs
Rustの組み込み関係の状況を調べてみると、STM32というベンダーのチップを使ったツールチェインやライブラリの整備が、だいぶん進んできているようです。こちらのサイトThe Embedded Rust Bookで詳しく解説が提供されていたりもします。
実装の方法は各種あるようですが、githubのこちらのアカウントstm32-rsに集積されているものが良さそうに見えます。今回はこれを試してみることにしました。
ターゲット
ターゲットは、手元にあったNucleo-F103というボードを使用します。このボードはSTM32F103というチップが載っています。デバッグアダプタも付いていて安価です。
Nucleoでなくても、STM32ベースのハードウェア(Discoveryシリーズや、Blue Pillなどいろいろあります)であれば、同様のことが可能だと思います。
ツールの準備
すでにRust環境は手元にあるかもしれません。インストール方法はこちらにあるとおりですが、念のため載せておきます。とても簡単ですね。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env
Rustのクロスコンパイル環境
STM32F103は、ARMのCortex-M3というアーキテクチャです。このアーキテクチャ用に、Rustのクロスコンパイル環境を適切に準備する必要があります。Cortex-M3の場合は、thumbv7m-none-eabi
がターゲット名となります。rustup
コマンドでターゲットを追加しておきます。
rustup target add thumbv7m-none-eabi
もし、Cortex-M0というアーキテクチャ(STM32F0xxが該当します)であれば、v7mの代わりにv6mを追加しておきます。
rustup target add thumbv6m-none-eabi
利用可能なターゲットの一覧は、次のように取得できます。
$ rustc --print target-list aarch64-fuchsia aarch64-linux-android ...中略... x86_64-unknown-redox x86_64-unknown-uefi
以前はRustで組み込みを試すにはnightlyに切り替える必要があったようですが、現在はstableのままで大丈夫なようです。お手元のrust環境にそのまま共存して使えます。
クロスツールのインストール
ARM用のバイナリファイルを取り扱うために、クロスツールを用意する必要があります。brew でインストールできます。
brew install armmbed/formulae/gcc-arm-none-eabi
openocdのインストール
Nucleoで走らせるためにはopenocdというツールを使用します。MacOSの場合は下記のようにして用意します。
brew install openocd
ソース
Lチカといっても、いろんな方法があります。適切な方法を見極めるのも目的の一つです。リポジトリを眺めてみたところ、HAL (Hardware Abstraction Layer)が整備されていましたので、今回はこれを使用してみます。
HALとは、ハードウェアの抽象化レイヤです。HALがあることによって、チップを変更しても修正は最小限で済ませることができるようになります。STM32はたくさんの種類があり、目的に応じて選択することができますが、HALがあることにより差異を吸収してくれます。
HAL以外にも、ボード固有のレイヤ、あるいはもっと低レベルの、チップのレジスタを直接操作するなどの方法がありますが、今回はHALを使ってみることにしました。HALのリポジトリ内にサンプルがたくさん用意されているので簡単に試せたというのが最大の理由ですが。
リポジトリをgithubからcloneします。
git clone [email protected]:stm32-rs/stm32f1xx-hal.git cd stm32f1xx-hal
サンプルを小改造する
サンプルは、リポジトリのexample
ディレクトリに用意されています。Lチカだけでも2種類あります。
- blinky.rs
- blinky_rtc.rs
しかしサンプルは、Nucleoボード用に作成されているわけではありませんので、ちょっとだけ修正する必要があります。
サンプルでは、LEDがGPIOのポートCの13番ピン(C13)に接続されていることが前提のようです。ところがNucleoはGPIO Aの5番に接続されています。これに合わせてソース example/blinky.rs
を下記のdiffのように書き換えます。
- // Acquire the GPIOC peripheral - let mut gpioc = dp.GPIOC.split(&mut rcc.apb2); + // Acquire the GPIOA peripheral + let mut gpioa = dp.GPIOA.split(&mut rcc.apb2); // Configure gpio C pin 13 as a push-pull output. The `crh` register is passed to the function // in order to configure the port. For pins 0-7, crl should be passed instead. - let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); + let mut led = gpioa.pa5.into_push_pull_output(&mut gpioa.crl); // Configure the syst timer to trigger an update every second let mut timer = Timer::syst(cp.SYST, 1.hz(), clocks);
ビルドする
まずはビルドしてみましょう。
example内にサンプルはたくさん入っていますが、cargoの引数に与えるだけで簡単に選んでビルドできます。また、チップの名称をfeaturesとして引数で渡します。これはstm32f1xx-hal
クレート固有の事項です。Cargo.toml
のfeaturesセクションで確認することができます。
ビルドの初回は依存クレートをダウンロードしたり、それらのコンパイルで少し時間がかかります。
cd stm32f103-hal cargo build --example blinky --release --features=stm32f103
実機で動かす
さて、ビルドが成功したら、次は実機で動かしてみます。
接続
USBでホストPCと接続します。Nucleoは今は珍しくなったUSB mini-Bです。ケーブルを確保しておきましょう。USBを接続したら、USBドライブとしてマウントされます。これはそのまま放置です。
ターミナルを一つ開いてopenocdを起動します。このとき、ターゲットに合わせて設定ファイルを指定します。設定ファイルはopenocdとともに/usr/local/share/openocd/scripts
にインストールされています。一度どんなものが用意されているか眺めてみると良いかもしれません。
Nucleoは、STLink-V2/1
というインターフェースなので、これ用の設定ファイルを指定します。またターゲットに合わせてstm32f1xを指定します。
openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg
エラーが出ずに、そのまま待機状態になることを確認します。
もしエラーが出て、プロンプトに戻ってきてしまう場合は、ファームウェアをアップデートする必要があるかもしれません。方法は後述します。
実行
openocd
を動作状態にしたまま、cargoコマンドでrunします。通常通りのビルドが終わった後にgdbが起動します。
cargo run --example blinky --release --features=stm32f103 ...中略... Finished release [optimized + debuginfo] target(s) in 3m 22s Running `arm-none-eabi-gdb target/thumbv7m-none-eabi/release/examples/blinky` GNU gdb (GNU Tools for Arm Embedded Processors 7-2018-q2-update) 8.1.0.20180315-git Copyright (C) 2018 Free Software Foundation, Inc. ...中略... Reading symbols from target/thumbv7m-none-eabi/release/examples/blinky...done. core::ptr::read_volatile (src=<optimized out>) at /rustc/3f55461efb25b3c8b5c5c3d829065cb032ec953b/src/libcore/ptr/mod.rs:920 920 /rustc/3f55461efb25b3c8b5c5c3d829065cb032ec953b/src/libcore/ptr/mod.rs: No such file or directory. semihosting is enabled Loading section .vector_table, size 0x400 lma 0x8000000 Loading section .text, size 0x392 lma 0x8000400 Loading section .rodata, size 0x58 lma 0x8000794 Start address 0x800050c, load size 2026 Transfer rate: 9 KB/sec, 675 bytes/write. DefaultPreInit () at /Users/xxx/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.10/src/lib.rs:564 564 pub unsafe extern "C" fn DefaultPreInit() {} (gdb)
自動的にopenocdと接続が行われ、バイナリがボードにロード(フラッシュメモリへの書き込み)が完了して、途中で実行が止まった状態となります。openocdへの接続やボードへのロードが自動でされるのは、.gdbinitファイルがプロジェクト内に用意されているためです。プロジェクトによっては用意されていないこともありますので、仕組みは把握しておきましょう。
gdbの下記のコマンドを実行します。
(gdb) continue
そうすると、ロードしたコードが実行されます。
無事LEDが点滅するのが確認できました!
semihostでHello World
ARMの組み込み系には、semihost
という仕組みがあります。これを使うと、実機側で生成したテキストによるログをホスト側で表示することができます。
hello.rs
というサンプルは、semihostによる Hello World です。こんなソースコードです。
//! Prints "Hello, world" on the OpenOCD console #![deny(unsafe_code)] #![no_main] #![no_std] use panic_semihosting as _; use stm32f1xx_hal as _; use cortex_m_semihosting::hprintln; use cortex_m_rt::entry; #[entry] fn main() -> ! { hprintln!("Hello, world!").unwrap(); loop {} }
先ほどのblink.rs
と同じように実行してみます。
cargo run --example hello --release --features=stm32f103 ... (gdb) continue
そうすると、openocd
側のターミナルを確認すると、openocd
の出力に混じってHello, World!が表示されているのがわかります。
Info : accepting 'gdb' connection on tcp/3333 Info : device id = 0x20036410 Info : flash size = 128kbytes undefined debug reason 7 - target needs reset semihosting is enabled target halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x0800050c msp: 0x20005000, semihosting Info : Padding image section 0 with 18 bytes target halted due to breakpoint, current mode: Thread xPSR: 0x61000000 pc: 0x2000003a msp: 0x20005000, semihosting target halted due to debug-request, current mode: Thread xPSR: 0x01000000 pc: 0x08000520 msp: 0x20005000, semihosting Info : halted: PC: 0x08000772 Hello, world!
これを使うと、ログを吐いたり、デバッグが便利にできそうです。
openocdがうまく動かない時は
実は当初、openocdを起動したときに、エラーが発生していました。これはボードのファームウェアのバージョンが古いせいでした。
こんなエラーです。このメッセージでは何が悪いのかよくわかりません。
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg Open On-Chip Debugger 0.10.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'. Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD adapter speed: 1000 kHz adapter_nsrst_delay: 100 none separate Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : clock speed 950 kHz in procedure 'init' in procedure 'ocd_bouncer'
この原因は、ファームウェアのバージョンが古いせいでした。Nucleoのファームウェアのアップデータは、STMicroelectronicsのサイトに用意されていますので、ダウンロードしてきます。
ダウンロードしたZipファイルを解凍すると、アップデータが2種類入っています。一方はWindows専用のバイナリ、もう一つがAllPlatforms
で、Linux/MacOS/Windowsなどどのプラットフォームでもアップデートが可能です。ただしJavaの実行環境が必要となります。
ボードをUSBで接続し、ターミナルから下記を実行します。
cd stsw-link007/AllPlatforms java -jar STLinkUpgrade.jar
画面が現れますので、Upgrade
ボタンを押してアップデートします。
アップデートに成功した後は、こんな感じでopenocdが正常に起動するようになります。
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f1x.cfg Open On-Chip Debugger 0.10.0 Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'. Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD adapter speed: 1000 kHz adapter_nsrst_delay: 100 none separate Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : Unable to match requested speed 1000 kHz, using 950 kHz Info : clock speed 950 kHz Info : STLINK v2 JTAG v34 API v2 SWIM v25 VID 0x0483 PID 0x374B Info : using stlink api v2 Info : Target voltage: 3.251613 Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
まとめ
開発ボードであるNucleo-F103を使って、RustでLチカを試してみました。stm32-rsとして整備されているHALとそのサンプルを使って簡単に試すことができました。
またsemihostを使って、伝統的なテキスト版Hello World
も試せました。シリアルポート等を使わずにデバッグメッセージを出せるので、いろいろと捗りそうです。